/*
 * XMPPServer.java
 *
 * Created on December 2, 2007, 3:46 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.xeerkat.xmpp.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.security.KeyStore;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ServerSocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManagerFactory;
import org.infoset.xml.Attribute;
import org.infoset.xml.Document;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.Element;
import org.infoset.xml.InfosetFactory;
import org.infoset.xml.Item;
import org.infoset.xml.Item.ItemType;
import org.infoset.xml.ItemConstructor;
import org.infoset.xml.ItemDestination;
import org.infoset.xml.ItemSource;
import org.infoset.xml.Name;
import org.infoset.xml.XMLException;
import org.infoset.xml.jaxp.StreamItemSource;
import org.infoset.xml.sax.SAXDocumentLoader;
import org.infoset.xml.util.DocumentDestination;
import org.infoset.xml.util.HandlerEngine;
import org.infoset.xml.util.HandlerEngine.Handler;
import org.restlet.Client;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.Method;
import org.restlet.data.Reference;
import org.xeerkat.xmpp.Base64Coder;
import org.xeerkat.xmpp.ID;
import org.xeerkat.xmpp.OrderedStanzaWriter;
import org.xeerkat.xmpp.Protocol;
import org.xeerkat.xmpp.StanzaReader;
import org.xeerkat.xmpp.server.db.Contact;
import org.xeerkat.xmpp.server.db.Group;
import org.xeerkat.xmpp.server.db.User;
import org.xeerkat.xmpp.server.db.XMPPDB;
import org.xeerkat.xmpp.server.tools.ServerConfiguration;

/**
 *
 * @author alex
 */
public class XMPPServer
{
   
   static final URI HOST_T = URI.create("http://www.xeerkat.org/O/host");
   static final URI AUTH_T = URI.create("http://www.xeerkat.org/O/auth");

   static Name ATOM_ENTRY = Name.create("{http://www.w3.org/2005/Atom}entry");
   static Name ATOM_CATEGORY = Name.create("{http://www.w3.org/2005/Atom}category");
   static Name ATOM_ID = Name.create("{http://www.w3.org/2005/Atom}id");


   static class EntryDestination extends DocumentDestination implements Handler {
      Handler parent;
      HandlerEngine engine;
      int level;
      List<Document> entries;
      EntryDestination(Handler parent,List<Document> entries) {
         this.parent = parent;
         this.level = 0;
         this.entries = entries;
      }
      public void init(HandlerEngine engine) {
         this.engine = engine;
         level = engine.getLevel();
      }
      public int getStartLevel() {
         return level;
      }
      public Handler pop() {
         return parent;
      }
      public void send(Item item)
         throws XMLException
      {
         super.send(item);
         if (item.getType()==ItemType.ElementEndItem && engine.getLevel()==level) {
            entries.add(getDocument());
         }
      }
   }
   class AutoConfProcess implements Runnable {
      public void run() {
         while (serverIsRunning) {
            try {
               autoConfigure();
               getLogger().info("Waiting "+(autoconfWait/1000)+"s to check configuration.");
               Thread.currentThread().sleep(autoconfWait);
            } catch (InterruptedException ex) {
               
            }
            
         }
         getLogger().info("Auto configuration thread exiting.");
      }
   }
   
   static class BindAddress {
      InetAddress address;
      int insecure;
      int secure;
      BindAddress(InetAddress address, int insecure,int secure) {
         this.address = address;
         this.insecure = insecure;
         this.secure = secure;
      }
   }
   
   class SocketPump implements Runnable,Stoppable {
      BindAddress bindAddress;
      ServerSocket socket;
      boolean running = false;
      boolean secure = false;
      SocketPump(BindAddress bindAddress) {
         this.bindAddress = bindAddress;
      }
      public void connect() 
         throws IOException
      {
         socket = bindAddress.address==null ? 
            ServerSocketFactory.getDefault().createServerSocket(bindAddress.insecure) :
            ServerSocketFactory.getDefault().createServerSocket(bindAddress.insecure,50,bindAddress.address);
         socket.setSoTimeout(60000);
      }
      public void run() {
         running = true;
         while (running) {
            try {
               Socket incoming = socket.accept();
               incoming.setSoTimeout((int)idleTimeout);
               incoming.setKeepAlive(true);
               log.info("Client on "+(secure? "secure" : "insecure")+" port.");
               ConnectedResource client = new ConnectedResource(secure,incoming);
               clients.addTask(client);
            } catch (java.net.SocketTimeoutException ex) {
               // Ignore this...
            } catch (XMLException ex) {
               log.log(Level.SEVERE,"Cannot establish stanza reader.",ex);
            } catch (IOException ex) {
               if (!socket.isClosed()) {
                  log.log(Level.SEVERE,"Cannot accept incoming connection.",ex);
               }
            }
         }
      }
      
      public void stop() 
      {
         try {
            running = false;
            socket.close();
         } catch (IOException ex) {
            log.log(Level.SEVERE,"Cannot accept incoming connection.",ex);
         }
      }
      
      public void kill() 
      {
         
      }
      
   }
   
   class SSLSocketPump extends SocketPump {
      SSLSocketPump(BindAddress bindAddress) {
         super(bindAddress);
         secure = true;
      }
      public void connect() 
         throws IOException
      {
         socket = bindAddress.address==null ? 
            sslContext.getServerSocketFactory().createServerSocket(bindAddress.secure) :
            sslContext.getServerSocketFactory().createServerSocket(bindAddress.secure,50,bindAddress.address);
         socket.setSoTimeout(60000);
      }
   }
   
   class IQHandler extends DocumentDestination {
      ConnectedResource resource;
      int level = 0;
      
      IQHandler(ConnectedResource resource)
      {
         this.resource = resource;
      }
      public void send(Item item) 
         throws XMLException
      {
         switch (item.getType()) {
            case ElementItem:
               if (level==0) {
                  super.send(InfosetFactory.getDefaultInfoset().createItemConstructor().createDocument());
               }
               super.send(item);
               level++;
               break;
            case ElementEndItem:
               super.send(item);
               level--;
               // TODO: a total hack because DocumentDestination is broken for element subtrees
               if (level==0) {
                  onDocument();
               }
               break;
            default:
               super.send(item);
         }
      }
      public void onDocument() 
         throws XMLException
      {
         Element iq = getDocument().getDocumentElement();
         String type = iq.getAttributeValue("type");
         if (type.equals("get")) {
            if (iq.getFirstElementNamed(Protocol.ROSTER_QUERY)==null) {
               resource.error(iq.getName(),iq.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_BAD_REQUEST,"Empty "+iq.getName()+" element.");
               return;
            }
            ItemDestination stanza = resource.getReceiver();
            ItemConstructor constructor = resource.outgoingStanzas.getConstructor();
            Element response = constructor.createElement(Protocol.IQ);
            response.setAttributeValue("to",resource.resourceId.toString());
            String id = iq.getAttributeValue("id");
            if (id!=null) {
               response.setAttributeValue("id",id);
            }
            response.setAttributeValue("type","result");
            stanza.send(response);
            stanza.send(constructor.createElement(Protocol.ROSTER_QUERY));
            
            try {
               Iterator<Contact> contacts = resource.user.getContacts();
               while (contacts.hasNext()) {
                  Contact contact = contacts.next();
                  Element item = constructor.createElement(Protocol.ROSTER_ITEM);
                  item.setAttributeValue("jid", contact.getXMPPID().toString());
                  if (contact.getName()!=null) {
                     item.setAttributeValue("name",contact.getName());
                  }
                  item.setAttributeValue("subscription",contact.getSubscription().toString());
                  if (contact.getAsk()!=Contact.Ask.None) {
                     item.setAttributeValue("ask","subscribe");
                  }
                  stanza.send(item);
                  Iterator<String> groups = contact.getGroupNames();
                  while (groups.hasNext()) {
                     stanza.send(constructor.createElement(Protocol.ROSTER_GROUP));
                     stanza.send(constructor.createCharacters(groups.next()));
                     stanza.send(constructor.createElementEnd(Protocol.ROSTER_GROUP));
                  }
                  stanza.send(constructor.createElementEnd(Protocol.ROSTER_ITEM));
               }
            } catch (SQLException ex) {
               throw new XMLException("Database error.",ex);
            }
            stanza.send(constructor.createElementEnd(Protocol.ROSTER_QUERY));
            stanza.send(constructor.createElementEnd(Protocol.IQ));
            
         } else if (type.equals("set")) {
            Element query =iq.getFirstElementNamed(Protocol.ROSTER_QUERY);
            if (query==null) {
               resource.error(iq.getName(),iq.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_BAD_REQUEST,"Empty "+iq.getName()+" element.");
               return;
            }
            Element item =query.getFirstElementNamed(Protocol.ROSTER_ITEM);
            if (item==null) {
               resource.error(iq.getName(),iq.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_BAD_REQUEST,"Empty "+query.getName()+" element.");
               return;
            }
            String jid = item.getAttributeValue("jid");
            if (jid==null) {
               resource.error(iq.getName(),iq.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_BAD_REQUEST,"Missing 'jid' attribute.");
               return;
            }
            ID contactId = null;
            try {
               contactId = new ID(jid);
               contactId = new ID(contactId.toXMPPId());
            } catch (IllegalArgumentException ex) {
               resource.error(iq.getName(),iq.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_BAD_REQUEST,"Bad ID: "+ex.getMessage());
               return;
            }
            try {
               Contact contact = resource.user.getContact(contactId);
               if ("remove".equals(item.getAttributeValue("subscription"))) {
                  if (contact!=null) {
                     contact.delete();
                     User otherLocal = database.getUser(contact.getXMPPID());
                     if (otherLocal!=null) {
                        Contact otherContact = otherLocal.getContact(resource.user.getXMPPId());
                        if (otherContact!=null) {
                           otherContact.setSubscription(Contact.Subscription.None);
                           otherContact.update();
                           switch (contact.getSubscription()) {
                              case To:
                                 switch (otherContact.getSubscription()) {
                                    case From:
                                       otherContact.setSubscription(Contact.Subscription.None);
                                       otherContact.update();
                                       break;
                                    case Both:
                                       otherContact.setSubscription(Contact.Subscription.To);
                                       otherContact.update();
                                 }
                                 break;
                              case From:
                                 switch (otherContact.getSubscription()) {
                                    case To:
                                       otherContact.setSubscription(Contact.Subscription.None);
                                       otherContact.update();
                                       break;
                                    case Both:
                                       otherContact.setSubscription(Contact.Subscription.From);
                                       otherContact.update();
                                 }
                                 break;
                              case Both:
                                 otherContact.setSubscription(Contact.Subscription.None);
                                 otherContact.update();
                                 break;
                           }
                        }
                        Iterator<ID> online = otherLocal.getResources();
                        while (online.hasNext()) {
                           ConnectedResource client = connected.get(online.next());
                           if (client!=null) {
                              switch (contact.getSubscription()) {
                                 case To:
                                    client.sendPresence(resource.user.getXMPPId(),"unsubscribe");
                                    break;
                                 case From:
                                    client.sendPresence(resource.user.getXMPPId(),"unsubscribed");
                                    break;
                                 case Both:
                                    client.sendPresence(resource.user.getXMPPId(),"unsubscribe");
                                    client.sendPresence(resource.user.getXMPPId(),"unsubscribed");
                                    break;
                              }
                           }
                        }
                     } else {
                        // route presense requests
                        // TODO: send unsubscribe for "to"
                        // TODO: send unsubscribed for "from"
                     }
                  }
               } else {
                  if (contact==null) {
                     contact = resource.user.addContact(contactId);
                  } else {
                     contact.removeAllGroups();
                  }
                  Iterator<Element> groupElements = item.getElementsByName(Protocol.ROSTER_GROUP);
                  while (groupElements.hasNext()) {
                     String name = groupElements.next().getText();
                     if (name.length()>0) {
                        Group group = resource.user.getGroup(name);
                        if (group==null) {
                           group = resource.user.addGroup(name);
                        }
                        contact.addGroup(group);
                     }
                  }

               }
               ItemDestination stanza = resource.getReceiver();
               ItemConstructor constructor = resource.outgoingStanzas.getConstructor();
               Element response = constructor.createElement(Protocol.IQ);
               response.setAttributeValue("to",resource.resourceId.toString());
               String id = iq.getAttributeValue("id");
               if (id!=null) {
                  response.setAttributeValue("id",id);
               }
               response.setAttributeValue("type","result");
               stanza.send(response);
               stanza.send(constructor.createElementEnd(Protocol.IQ));
            } catch (SQLException ex) {
               log.log(Level.SEVERE,"Database error.",ex);
               resource.error(iq.getName(),iq.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_INTERNAL_SERVER_ERROR,"Database error.");
            }
         } else {
            resource.error(iq.getName(),iq.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_BAD_REQUEST,"Unrecognized type value '"+type+"'");
         }
      }
   }
   
   protected class ConnectedResource implements Runnable,Stoppable {
      
      boolean secure;
      UUID id;
      User user;
      ID resourceId;
      Socket plain;
      Socket connection;
      Reader input;
      Writer output;
      StanzaReader incomingStanzas;
      OrderedStanzaWriter outgoingStanzas;
      Host host;
      Map<Name,String> properties;
      boolean lastUnavailable;
      ConnectedResource(boolean secure,Socket connection) 
         throws IOException,XMLException
      {
         this.id = UUID.randomUUID();
         this.user = null;
         this.secure = secure;
         this.connection = connection;
         this.lastUnavailable = false;
         initStreams();
      }
      protected void initStreams() 
         throws IOException,XMLException
      {
         this.input = new InputStreamReader(connection.getInputStream(),"UTF-8");
         this.output = new OutputStreamWriter(this.connection.getOutputStream(),"UTF-8");
         this.incomingStanzas = new StanzaReader(log,new StreamItemSource(null,input,true));
         this.outgoingStanzas = new OrderedStanzaWriter(log,output);
      }
      public ItemDestination getReceiver() {
         return outgoingStanzas.next();
      }
      
      public void error(Name offendingType,String id,String type,Name condition,String message) 
         throws XMLException
      {
         if (!serverIsRunning) {
            return;
         }
         ItemConstructor constructor = outgoingStanzas.getConstructor();
         ItemDestination stanza = outgoingStanzas.next();

         Element top = constructor.createElement(offendingType);
         if (id!=null) {
            top.setAttributeValue("id",id);
         }
         top.setAttributeValue("to",resourceId.toString());
         top.setAttributeValue("type","error");
         stanza.send(top);
         
         Element error = constructor.createElement(Protocol.ERROR);
         error.setAttributeValue("type",type);
         stanza.send(error);
         
         stanza.send(constructor.createElement(condition));
         stanza.send(constructor.createElementEnd(condition));
         stanza.send(constructor.createElement(Protocol.STANZAS_ERROR_TEXT));
         stanza.send(constructor.createCharacters(message));
         stanza.send(constructor.createElementEnd(Protocol.STANZAS_ERROR_TEXT));
         
         stanza.send(constructor.createElementEnd(Protocol.ERROR));
         stanza.send(constructor.createElementEnd(offendingType));
      }
      public void sendPresence(ID from,String type) 
         throws XMLException
      {
         if (!serverIsRunning) {
            return;
         }
         log.info((type==null ? "available" : type)+" presence from "+from+" to "+resourceId);
         ItemConstructor constructor = outgoingStanzas.getConstructor();
         ItemDestination stanza = outgoingStanzas.next();
         Element start = constructor.createElement(Protocol.PRESENCE);
         if (from!=null) {
            start.setAttributeValue("from",from.toString());
         }
         start.setAttributeValue("to",resourceId.toString());
         if (type!=null) {
            start.setAttributeValue("type",type);
         }
         stanza.send(start);
         stanza.send(constructor.createElementEnd(Protocol.PRESENCE));
      }
      public void run() {
         try {
            log.info("Client on "+(secure ? "secure" : "insecure")+" connection...");
            if (!serverIsRunning) {
               return;
            }
            try {
               incomingStanzas.start();
            } catch (XMLException ex) {
               log.warning("Bad XML from client: "+ex.getMessage());
               connection.close();
               return;
            }
            String domain = incomingStanzas.getProperties().get(StanzaReader.TO_PROPERTY);
            host = hosts.get(domain);
            properties = new TreeMap<Name,String>();
            properties.put(StanzaReader.ID_PROPERTY,id.toString());
            properties.put(StanzaReader.VERSION_PROPERTY,"1.0");
            properties.put(StanzaReader.FROM_PROPERTY, domain);
            if (!serverIsRunning) {
               return;
            }
            outgoingStanzas.start(properties);
            if (host==null) {
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_HOST_UNKNOWN,"Host "+domain+" does not exist.");
               outgoingStanzas.end();
               connection.close();
               return;
            }
            if (!serverIsRunning) {
               return;
            }
            ItemConstructor constructor = outgoingStanzas.getConstructor();
            ItemDestination stanza = outgoingStanzas.next();
            stanza.send(constructor.createElement(Protocol.FEATURES));
            if (!secure) {
               stanza.send(constructor.createElement(Protocol.STARTTLS));
               stanza.send(constructor.createElement(Protocol.STARTTLS_REQUIRED));
               stanza.send(constructor.createElementEnd(Protocol.STARTTLS_REQUIRED));
               stanza.send(constructor.createElementEnd(Protocol.STARTTLS));
               
            }
               
            stanza.send(constructor.createElement(Protocol.MECHANISMS));
            stanza.send(constructor.createElement(Protocol.MECHANISM));
            stanza.send(constructor.createCharacters("PLAIN"));
            stanza.send(constructor.createElementEnd(Protocol.MECHANISM));
            stanza.send(constructor.createElementEnd(Protocol.MECHANISMS));
            
            stanza.send(constructor.createElementEnd(Protocol.FEATURES));
            
            if (!secure) {
               // Wait for starttls
               log.info("Waiting for starttls...");
               if (!serverIsRunning) {
                  return;
               }
               DocumentDestination starttls = new DocumentDestination();
               if (incomingStanzas.next(starttls)) {
                  if (starttls.getDocument().getDocumentElement().getName().equals(Protocol.STARTTLS)) {
                     if (!serverIsRunning) {
                        return;
                     }
                     stanza = outgoingStanzas.next();
                     stanza.send(constructor.createElement(Protocol.STARTTLS_PROCEED));
                     stanza.send(constructor.createElementEnd(Protocol.STARTTLS_PROCEED));
                     plain = connection;
                     String hostname = plain.getInetAddress().getHostName();
                     log.info("Proceeed over "+plain.getInetAddress()+" as "+hostname+" ...");
                     connection = sslContext.getSocketFactory().createSocket(plain,hostname, plain.getPort(), false);
                     connection.setSoTimeout((int)(idleTimeout/3)+(int)(idleTimeout*0.01));
                     connection.setKeepAlive(true);
                     ((SSLSocket)connection).setUseClientMode(false);
                     ((SSLSocket)connection).startHandshake();
                     secure = true;
                     log.info("Switched to SSL...");
                     log.info("Re-initializing streams...");
                     initStreams();
                     if (!serverIsRunning) {
                        return;
                     }
                     try {
                        incomingStanzas.start();
                     } catch (XMLException ex) {
                        log.warning("Bad XML from client: "+ex.getMessage());
                        connection.close();
                        return;
                     }
                     domain = incomingStanzas.getProperties().get(StanzaReader.TO_PROPERTY);
                     host = hosts.get(domain);
                     if (!serverIsRunning) {
                        return;
                     }
                     outgoingStanzas.start(properties);
                     if (host==null) {
                        streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_HOST_UNKNOWN,"Host "+domain+" does not exist.");
                        outgoingStanzas.end();
                        connection.close();
                     } else {
                        if (!serverIsRunning) {
                           return;
                        }
                        constructor = outgoingStanzas.getConstructor();
                        stanza = outgoingStanzas.next();
                        stanza.send(constructor.createElement(Protocol.FEATURES));
                        stanza.send(constructor.createElement(Protocol.MECHANISMS));
                        stanza.send(constructor.createElement(Protocol.MECHANISM));
                        stanza.send(constructor.createCharacters("PLAIN"));
                        stanza.send(constructor.createElementEnd(Protocol.MECHANISM));
                        stanza.send(constructor.createElementEnd(Protocol.MECHANISMS));
                        stanza.send(constructor.createElementEnd(Protocol.FEATURES));
                     }
                  } else {
                     log.info("Unexpected element: "+starttls.getDocument().getDocumentElement().getName());
                     if (!serverIsRunning) {
                        return;
                     }
                     stanza = outgoingStanzas.next();
                     stanza.send(constructor.createElement(Protocol.STARTTLS_FAILURE));
                     stanza.send(constructor.createElementEnd(Protocol.STARTTLS_FAILURE));
                     outgoingStanzas.end();
                     connection.close();
                     return;
                  }
               } else {
                  streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_NOT_AUTHORIZED,"starttls expected.");
                  outgoingStanzas.end();
                  connection.close();
                  return;
               }
            }
            
            
            if (!serverIsRunning) {
               return;
            }
            log.info("Waiting for auth...");
            // wait for auth
            DocumentDestination authRequest = new DocumentDestination();
            if (!incomingStanzas.next(authRequest)) {
               log.info("No auth stanza.");
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_NOT_AUTHORIZED,"auth expected.");
               outgoingStanzas.end();
               connection.close();
               return;
            }
            Element auth = authRequest.getDocument().getDocumentElement();
            if (!auth.getName().equals(Protocol.AUTH)) {
               log.info("Bad auth from client for bind: "+auth.getName());
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_NOT_AUTHORIZED,"auth expected.");
               outgoingStanzas.end();
               connection.close();
               return;
            }
            String mechanism = auth.getAttributeValue("mechanism");
            if (mechanism==null || !mechanism.equals("PLAIN")) {
               log.info("No mechanism matched: "+mechanism);
               if (!serverIsRunning) {
                  return;
               }
               ItemDestination failure = outgoingStanzas.next();
               failure.send(constructor.createElement(Protocol.FAILURE));
               failure.send(constructor.createElement(Protocol.INVALID_MECHANISM));
               failure.send(constructor.createElementEnd(Protocol.INVALID_MECHANISM));
               failure.send(constructor.createElementEnd(Protocol.FAILURE));
               outgoingStanzas.end();
               connection.close();
               return;
            }
            String authData = Base64Coder.decode(auth.getText());
            String [] authParts = authData.split("\0");
            if (authParts.length!=3) {
               log.info("Bad auth: "+authData);
               // bad auth string
               if (!serverIsRunning) {
                  return;
               }
               ItemDestination failure = outgoingStanzas.next();
               failure.send(constructor.createElement(Protocol.FAILURE));
               failure.send(constructor.createElement(Protocol.INCORRECT_ENCODING));
               failure.send(constructor.createElementEnd(Protocol.INCORRECT_ENCODING));
               failure.send(constructor.createElementEnd(Protocol.FAILURE));
               outgoingStanzas.end();
               connection.close();
               return;
            }
            
            ID jid = null;
            try {
               jid = authParts[0].length()==0 ? new ID(authParts[1]+"@"+domain) : new ID(authParts[0]);
            } catch (IllegalArgumentException ex) {
               log.info("Bad ID: "+authParts[0]);
               // bad auth string
               if (!serverIsRunning) {
                  return;
               }
               ItemDestination failure = outgoingStanzas.next();
               failure.send(constructor.createElement(Protocol.FAILURE));
               failure.send(constructor.createElement(Protocol.INCORRECT_ENCODING));
               failure.send(constructor.createElementEnd(Protocol.INCORRECT_ENCODING));
               failure.send(constructor.createElementEnd(Protocol.FAILURE));
               outgoingStanzas.end();
               connection.close();
               return;
            }
            if (!jid.getName().equals(authParts[1])) {
               if (!serverIsRunning) {
                  return;
               }
               ItemDestination failure = outgoingStanzas.next();
               failure.send(constructor.createElement(Protocol.FAILURE));
               failure.send(constructor.createElement(Protocol.NOT_AUTHORIZED));
               failure.send(constructor.createElementEnd(Protocol.NOT_AUTHORIZED));
               failure.send(constructor.createElementEnd(Protocol.FAILURE));
               outgoingStanzas.end();
               connection.close();
               return;
            }
            boolean valid = authenticate(host,authParts[1],authParts[2]);
            if (!serverIsRunning) {
               return;
            }
            if (valid) {
               ItemDestination success = outgoingStanzas.next();
               success.send(constructor.createElement(Protocol.SUCCESS));
               success.send(constructor.createElementEnd(Protocol.SUCCESS));
            } else {
               ItemDestination failure = outgoingStanzas.next();
               failure.send(constructor.createElement(Protocol.FAILURE));
               failure.send(constructor.createElement(Protocol.NOT_AUTHORIZED));
               failure.send(constructor.createElementEnd(Protocol.NOT_AUTHORIZED));
               failure.send(constructor.createElementEnd(Protocol.FAILURE));
               outgoingStanzas.end();
               connection.close();
               return;
            }
            log.info(jid+" authenticated.");
            
            user = database.getUser(jid);
            if (user==null) {
               user = database.createUser(jid);
            }
            

            log.info(jid+" Re-initializing streams...");
            
            if (!serverIsRunning) {
               return;
            }
            ItemSource source = new StreamItemSource(null,input,true);
            incomingStanzas = new StanzaReader(log,source);
            incomingStanzas.start();
            
            outgoingStanzas = new OrderedStanzaWriter(log,output);
            outgoingStanzas.start(properties);
            stanza = outgoingStanzas.next();
            stanza.send(constructor.createElement(Protocol.FEATURES));
            stanza.send(constructor.createElement(Protocol.BIND));
            stanza.send(constructor.createElementEnd(Protocol.BIND));
            stanza.send(constructor.createElement(Protocol.SESSION));
            stanza.send(constructor.createElementEnd(Protocol.SESSION));
            stanza.send(constructor.createElementEnd(Protocol.FEATURES));

            if (!serverIsRunning) {
               return;
            }
            DocumentDestination bindRequest = new DocumentDestination();
            if (!incomingStanzas.next(bindRequest)) {
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_NOT_AUTHORIZED,"bind expected.");
               outgoingStanzas.end();
               connection.close();
               return;
            }
            
            Element bindIQ = bindRequest.getDocument().getDocumentElement();
            if (!bindIQ.getName().equals(Protocol.IQ) || !"set".equals(bindIQ.getAttributeValue("type"))) {
               log.info(jid+" Bad IQ from client for bind: "+bindIQ.getName());
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_NOT_AUTHORIZED,"bind expected.");
               outgoingStanzas.end();
               connection.close();
               return;
            }
            
            if (!serverIsRunning) {
               return;
            }
            Element bind = bindIQ.getFirstElementNamed(Protocol.BIND);
            if (bind==null) {
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_BAD_FORMAT,"bind element missing.");
               outgoingStanzas.end();
               connection.close();
               return;
            }
            Element resourceE = bind.getFirstElementNamed(Protocol.RESOURCE);
            String resource = resourceE==null ? "agent" : resourceE.getText();
            if (resource.length()==0) {
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_BAD_FORMAT,"resource string is emtpy.");
               outgoingStanzas.end();
               connection.close();
               return;
            }

            resourceId = user.goOnline(resource);
            
            log.info(jid+" Binding request to "+resourceId);
            connected(resourceId,this);
            
            if (!serverIsRunning) {
               return;
            }
            stanza = outgoingStanzas.next();
            Element iq = constructor.createElement(Protocol.IQ);
            iq.setAttributeValue("from",domain);
            iq.setAttributeValue("type","result");
            iq.setAttributeValue("id",bindIQ.getAttributeValue("id"));
            stanza.send(iq);
            stanza.send(constructor.createElement(Protocol.BIND));
            stanza.send(constructor.createElement(Protocol.JID));
            stanza.send(constructor.createCharacters(resourceId.toString()));
            stanza.send(constructor.createElementEnd(Protocol.JID));
            stanza.send(constructor.createElementEnd(Protocol.BIND));
            stanza.send(constructor.createElementEnd(Protocol.IQ));
            
            if (!serverIsRunning) {
               return;
            }
            DocumentDestination sessionRequest = new DocumentDestination();
            if (!incomingStanzas.next(sessionRequest)) {
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_NOT_AUTHORIZED,"session expected.");
               outgoingStanzas.end();
               connection.close();
               return;
            }
            Element sessionIQ = sessionRequest.getDocument().getDocumentElement();
            if (!sessionIQ.getName().equals(Protocol.IQ) || !"set".equals(sessionIQ.getAttributeValue("type"))) {
               log.info(resourceId+" Bad IQ from client for session: "+bindIQ.getName());
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_NOT_AUTHORIZED,"session expected.");
               outgoingStanzas.end();
               connection.close();
               return;
            }
            Element session = sessionIQ.getFirstElementNamed(Protocol.SESSION);
            if (session==null) {
               log.info(resourceId+" No session requested.");
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_BAD_FORMAT,"session element is missing.");
               outgoingStanzas.end();
               connection.close();
               return;
            }
            if (!serverIsRunning) {
               return;
            }
            stanza = outgoingStanzas.next();
            iq = constructor.createElement(Protocol.IQ);
            iq.setAttributeValue("from",domain);
            iq.setAttributeValue("type","result");
            iq.setAttributeValue("id",sessionIQ.getAttributeValue("id"));
            stanza.send(iq);
            stanza.send(constructor.createElementEnd(Protocol.IQ));
            log.info(resourceId+" Session established.");
            
            ItemDestination handler = new ItemDestination() {
               int level = 0;
               ItemDestination delegate = null;
               public void send(Item item) 
                  throws XMLException
               {
                  switch (item.getType()) {
                     case ElementItem:
                        if (level==0) {
                           lastUnavailable = false;
                           Element start = (Element)item;
                           Name type = start.getName();
                           if (type.equals(Protocol.MESSAGE)) {
                              String toValue = start.getAttributeValue("to");
                              if (toValue==null) {
                                 log.warning("Bad message received: no 'to' attribute from "+resourceId);
                                 error(type,start.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_BAD_REQUEST,"The 'to' attribute is missing.");
                                 delegate = new ItemDestination() {
                                    public void send(Item item) {
                                    }
                                 };
                                 return;
                              }
                              try {
                                 ID recipient = new ID(toValue);
                                 delegate = getRecipientDestination(recipient);
                                 if (delegate==null) {
                                    log.warning(resourceId+" Message to offline id "+toValue);
                                 } else {
                                    log.info("Message from "+resourceId+" to "+recipient+" delivered.");
                                 }
                              } catch (IllegalArgumentException ex) {
                                 error(type,start.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_JID_MALFORMED,"Malformed ID: "+toValue);
                              }

                           } else if (type.equals(Protocol.PRESENCE)) {
                              String toValue = start.getAttributeValue("to");
                              String presenceType = start.getAttributeValue("type");
                              if (toValue==null) {
                                 if (presenceType!=null && !presenceType.equals("unavailable")) {
                                    // Only untyped or unavailable are processed
                                    error(type,start.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_BAD_REQUEST,"Type '"+presenceType+"' requires a 'to' attribute.");
                                 } else {
                                    try {
                                       lastUnavailable = "unavailable".equals(presenceType);
                                       log.info(resourceId+(lastUnavailable ? " Unavailable" : " Available")+" presence to contacts.");
                                       notifyPresence(user,resourceId,!lastUnavailable);
                                    } catch (SQLException ex) {
                                       log.log(Level.SEVERE,"Database error.",ex);
                                       error(type,start.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_INTERNAL_SERVER_ERROR,"Database error.");
                                    }
                                 }
                              } else {
                                 try {
                                    ID recipient = new ID(toValue);
                                    if ("unavailable".equals(presenceType)) {
                                       log.info(resourceId+" Unavailable to "+toValue);
                                       start.setAttributeValue("from", resourceId.toString());
                                       delegate = getRecipientDestination(recipient);
                                       if (delegate==null) {
                                          log.warning(resourceId+" Presence to offline id "+toValue);
                                       }
                                    } else if ("subscribe".equals(presenceType)) {
                                       log.info(resourceId+" Subscribe request to "+toValue);
                                       try {
                                          Contact contact = user.getContact(recipient);
                                          if (contact!=null) {
                                             if (contact.getSubscription()==Contact.Subscription.To ||
                                                 contact.getSubscription()==Contact.Subscription.Both) {
                                                return;
                                             }
                                             start.setAttributeValue("from", resourceId.toString());
                                             delegate = getRecipientDestination(recipient);
                                             if (delegate==null) {
                                                contact.setAsk(Contact.Ask.SubscribeUnsent);
                                                contact.update();
                                             } else {
                                                contact.setAsk(Contact.Ask.Subscribe);
                                                contact.update();
                                             }
                                             log.info("subscription="+user.getContact(recipient).getSubscription());
                                          }
                                       } catch (SQLException ex) {
                                          log.log(Level.SEVERE,"Database error.",ex);
                                          error(type,start.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_INTERNAL_SERVER_ERROR,"Database error.");
                                       }
                                    } else if ("subscribed".equals(presenceType)) {
                                       log.info(resourceId+" Subscription approved for "+toValue);
                                       try {
                                          Contact contact = user.getContact(recipient);
                                          if (contact==null) {
                                             // Add the contact if it doesn't exist and do a roster push
                                             contact = user.addContact(recipient);
                                             Iterator<ID> online = user.getResources();
                                             while (online.hasNext()) {
                                                ID onlineResource = online.next();
                                                if (!onlineResource.equals(resourceId)) {
                                                   ConnectedResource client = connected.get(onlineResource);

                                                   // TODO: do roster push

                                                }
                                             }
                                          }
                                          // Change the subscription
                                          switch (contact.getSubscription()) {
                                             case None:
                                                contact.setSubscription(Contact.Subscription.From);
                                                contact.update();
                                                break;
                                             case To:
                                                contact.setSubscription(Contact.Subscription.Both);
                                                contact.update();
                                                break;
                                          }
                                          // Do a roster push of the changed state
                                          Iterator<ID> online = user.getResources();
                                          while (online.hasNext()) {
                                             ID onlineResource = online.next();
                                             ConnectedResource client = connected.get(onlineResource);

                                             // TODO: do roster push
                                          }

                                          User otherLocal = database.getUser(recipient);
                                          if (otherLocal!=null) {

                                             // subscribe other local
                                             contact = otherLocal.getContact(user.getXMPPId());
                                             if (contact!=null) {

                                                if (contact.getAsk()==Contact.Ask.Subscribe ||
                                                    contact.getAsk()==Contact.Ask.SubscribeUnsent) {
                                                   switch (contact.getSubscription()) {
                                                      case None:
                                                         contact.setAsk(Contact.Ask.None);
                                                         contact.setSubscription(Contact.Subscription.To);
                                                         contact.update();
                                                         break;
                                                      case From:
                                                         contact.setAsk(Contact.Ask.None);
                                                         contact.setSubscription(Contact.Subscription.Both);
                                                         contact.update();
                                                         break;
                                                   }

                                                   online = otherLocal.getResources();
                                                   while (online.hasNext()) {
                                                      ID onlineResource = online.next();
                                                      ConnectedResource client = connected.get(onlineResource);
                                                      // TODO: do a roster push
                                                      client.sendPresence(resourceId, "subscribed");
                                                      client.sendPresence(resourceId, null);
                                                   }
                                                }

                                             }
                                          } else {
                                             // route message
                                             // TODO: implement
                                          }
                                       } catch (SQLException ex) {
                                          log.log(Level.SEVERE,"Database error.",ex);
                                          error(type,start.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_INTERNAL_SERVER_ERROR,"Database error.");
                                       }
                                    } else if ("unsubscribe".equals(presenceType)) {
                                       log.info(resourceId+" Unsubscribe request to "+toValue);
                                       try {
                                          Contact contact = user.getContact(recipient);
                                          if (contact!=null) {
                                             switch (contact.getSubscription()) {
                                                case None:
                                                case From:
                                                   return;
                                                case To:
                                                   contact.setSubscription(Contact.Subscription.None);
                                                   contact.update();
                                                   break;
                                                case Both:
                                                   contact.setSubscription(Contact.Subscription.From);
                                                   contact.update();
                                                   
                                             }
                                             
                                             Iterator<ID> online = user.getResources();
                                             while (online.hasNext()) {
                                                ID onlineResource = online.next();
                                                if (!onlineResource.equals(resourceId)) {
                                                   ConnectedResource client = connected.get(onlineResource);
                                                   
                                                   // TODO: do roster push
                                                   
                                                }
                                             }
                                             
                                             User otherLocal = database.getUser(recipient);
                                             if (otherLocal!=null) {
                                                
                                                // all local
                                                
                                                contact = otherLocal.getContact(resourceId);
                                                if (contact!=null) {
                                                   switch (contact.getSubscription()) {
                                                      case None:
                                                      case To:
                                                         break;
                                                      case From:
                                                         contact.setSubscription(Contact.Subscription.None);
                                                         contact.update();
                                                         break;
                                                      case Both:
                                                         contact.setSubscription(Contact.Subscription.To);
                                                         contact.update();
                                                   }
                                                }
                                                
                                                online = otherLocal.getResources();
                                                while (online.hasNext()) {
                                                   ID onlineResource = online.next();
                                                   if (!onlineResource.equals(resourceId)) {
                                                      ConnectedResource client = connected.get(onlineResource);
                                                      client.sendPresence(resourceId, "unsubscribe");
                                                   }
                                                }

                                                sendPresence(recipient,"unsubscribed");
                                                sendPresence(recipient,"unavailable");
                                             } else {
                                                // TODO: route request
                                             }
                                          }
                                       } catch (SQLException ex) {
                                          log.log(Level.SEVERE,"Database error.",ex);
                                          error(type,start.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_INTERNAL_SERVER_ERROR,"Database error.");
                                       }
                                    } else if ("unsubscribed".equals(presenceType)) {
                                       log.info(resourceId+" Subscription canceled for "+toValue);
                                       try {
                                          Contact contact = user.getContact(recipient);
                                          if (contact!=null) {
                                             switch (contact.getSubscription()) {
                                                case None:
                                                case To:
                                                   break;
                                                case From:
                                                   contact.setSubscription(Contact.Subscription.None);
                                                   contact.update();
                                                   break;
                                                case Both:
                                                   contact.setSubscription(Contact.Subscription.To);
                                                   contact.update();
                                                   break;
                                             }
                                             
                                             Iterator<ID> online = user.getResources();
                                             while (online.hasNext()) {
                                                ID onlineResource = online.next();
                                                if (!onlineResource.equals(resourceId)) {
                                                   ConnectedResource client = connected.get(onlineResource);
                                                   
                                                   // TODO: do roster push
                                                   
                                                }
                                             }
                                             
                                             User otherLocal = database.getUser(recipient);
                                             if (otherLocal!=null) {
                                                // all local
                                                contact = otherLocal.getContact(resourceId);
                                                if (contact!=null) {
                                                   switch (contact.getSubscription()) {
                                                      case None:
                                                      case From:
                                                         break;
                                                      case To:
                                                         contact.setSubscription(Contact.Subscription.None);
                                                         contact.update();
                                                         break;
                                                      case Both:
                                                         contact.setSubscription(Contact.Subscription.From);
                                                         contact.update();
                                                         break;

                                                   }
                                                }
                                                
                                                online = otherLocal.getResources();
                                                while (online.hasNext()) {
                                                   ID otherResource = online.next();
                                                   ConnectedResource client = connected.get(otherResource);
                                                   // TODO: do a roster push
                                                   client.sendPresence(resourceId, "unsubscribe");
                                                   client.sendPresence(resourceId, "unavailable");
                                                   
                                                }
                                             } else {
                                                // TODO: route request
                                             }
                                          }
                                       } catch (SQLException ex) {
                                          log.log(Level.SEVERE,"Database error.",ex);
                                          error(type,start.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_INTERNAL_SERVER_ERROR,"Database error.");
                                       }
                                    } else {
                                       log.info(resourceId+" Available presence to "+toValue);
                                       start.setAttributeValue("from", resourceId.toString());
                                       delegate = getRecipientDestination(recipient);
                                       if (delegate==null) {
                                          log.warning(resourceId+" Presence to offline id "+toValue);
                                       }
                                    }
                                 } catch (IllegalArgumentException ex) {
                                    error(type,start.getAttributeValue("id"),"cancel",Protocol.STANZAS_ERROR_JID_MALFORMED,"Malformed ID: "+toValue);
                                 }
                              }
                           } else if (type.equals(Protocol.IQ)) {
                              delegate = new IQHandler(ConnectedResource.this);
                           } else {
                              StringBuilder builder = new StringBuilder();
                              for (Attribute att : start.getAttributes().values()) {
                                 builder.append(" ");
                                 builder.append(att.getName().toString());
                                 builder.append("=");
                                 builder.append(att.getText());
                              }
                              log.info(resourceId+" Stanza type: "+start.getName()+builder.toString());
                           }
                           
                        }
                        level++;
                        break;
                     case ElementEndItem:
                        level--;
                        break;
                  }
                  if (delegate!=null) {
                     delegate.send(item);
                     if (level==0 && item.getType()==Item.ItemType.ElementEndItem) {
                        delegate = null;
                     }
                  }
               }
            };
            boolean gotStanza = false;
            long elapsed = 0;
            long heartbeatInterval = idleTimeout*2/3;
            long lastHeartbeat = 0;
            do {
               try {
                  gotStanza = incomingStanzas.next(handler);
                  long now = System.currentTimeMillis();
                  elapsed = now-incomingStanzas.getLastReceivedAt();
               } catch (Exception ex) {
                  if (serverIsRunning) {
                     gotStanza = false;
                     long now = System.currentTimeMillis();
                     elapsed = now-incomingStanzas.getLastReceivedAt();
                     // hack for timeout being masked by stream parser.  The parse catches the exception
                     // but does not set the cause!
                     if (ex.getMessage().indexOf("timed out")>=0) {
                        // we've just timed out because the read timed out.  The keep alives are going through
                        // so we'll fake like we've got a stanza
                        gotStanza = true;
                     }
                     if (!gotStanza) {
                        log.log(Level.SEVERE,ex.getMessage(),ex);
                        if (ex.getCause()!=null) {
                           log.severe("Cause: "+ex.getCause().getClass().getName());
                        }
                     }
                  }
               }
               if (serverIsRunning && elapsed<idleTimeout && sendHeartbeat && (System.currentTimeMillis()-lastHeartbeat)>heartbeatInterval) {
                  sendPresence(null, "probe");
                  lastHeartbeat = System.currentTimeMillis();
               }
               
            } while (serverIsRunning && gotStanza && elapsed<idleTimeout);
            if (elapsed>=idleTimeout) {
               log.warning(resourceId+" timed out, closing.");
            }
            if (outgoingStanzas.isActive()) {
               outgoingStanzas.end();
            }
            log.info(resourceId+" Stream finished.");
            if (!lastUnavailable) {
               notifyPresence(user,resourceId,false);
               lastUnavailable = true;
            }
         } catch (XMLException ex) {
            log.log(Level.WARNING,"General XML exception: "+ex.getMessage(),ex);
            try {
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_XML_NOT_WELL_FORMED,ex.getMessage());
               outgoingStanzas.end();
            } catch (XMLException ignore) {
            }
         } catch (IOException ex) {
            log.log(Level.WARNING,"I/O error.",ex);
            try {
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_INTERNAL_SERVER_ERROR,"I/O error.");
               outgoingStanzas.end();
            } catch (XMLException ignore) {
            }
         } catch (SQLException ex) {
            log.log(Level.SEVERE,"Database error.",ex);
            try {
               streamError(outgoingStanzas.getConstructor(),outgoingStanzas.next(),Protocol.STREAMS_ERROR_INTERNAL_SERVER_ERROR,"database request failed.");
               outgoingStanzas.end();
            } catch (XMLException ignore) {
            }
         } finally {
            if (resourceId!=null) {
               log.info(resourceId+" has disconnected.");
               try {
                  disconnect(resourceId,this);
               } catch (SQLException ex) {
                  log.log(Level.SEVERE,"Database error.",ex);
               }
            }
            if (!lastUnavailable) {
               try {
                  notifyPresence(user,resourceId,false);
               } catch (SQLException ex) {
                  log.log(Level.SEVERE,"Database error.",ex);
               } catch (XMLException ex) {
                  log.log(Level.SEVERE,"XML error.",ex);
               }
                  
            }
            if (!connection.isClosed()) {
               try {
                  connection.close();
               } catch (IOException ex) {
               }
            }
         }
      }
      public void stop() {
         if (outgoingStanzas.isActive()) {
            log.info("Ending connection for client "+resourceId);
            try {
               connection.setSoTimeout(500);
               outgoingStanzas.end();
            } catch (Exception ex) {
               log.log(Level.SEVERE,"Cannot end outgoing stanzas.",ex);
            }
         }
      }
      
      public void kill() {
         
         try {
            log.info("Killing client connection for "+resourceId);
            connection.setSoTimeout(500);
            connection.close();
         } catch (IOException ex) {
         }
         log.info(resourceId+" has been killed.");
      }
   }
   
   List<BindAddress> addresses;
   Logger log;
   File keystorePath;
   String keystorePassword;
   String keyPassword;
   SSLContext sslContext;
   ThreadTaskPool pumps;
   ThreadTaskPool clients;
   Map<String,Host> staticHosts;
   Map<String,Host> hosts;
   Map<ID,ConnectedResource> connected;
   XMPPDB database;
   long autoconfWait = 3*60*1000;
   List<ServerConfiguration.Link> autoconfs;
   Thread autoconfThread;
   AutoConfProcess autoconfProc;
   boolean sendHeartbeat;
   long idleTimeout = 2*60*1000;
   boolean serverIsRunning;
   
   /** Creates a new instance of XMPPServer */
   public XMPPServer(XMPPDB database)
   {
      this.database = database;
      this.addresses = new ArrayList<BindAddress>();
      this.log = Logger.getLogger(XMPPServer.class.getName());
      this.keystorePath = new File(System.getProperty("user.home") + File.separator + ".keystore");
      this.keystorePassword = "";
      this.keyPassword = "";
      this.pumps = new ThreadTaskPool(log,new ThreadGroup("pumps"),0);
      this.clients = new ThreadTaskPool(log,new ThreadGroup("clients"),10);
      this.hosts = new TreeMap<String,Host>();
      this.staticHosts = new TreeMap<String,Host>();
      this.connected = new TreeMap<ID,ConnectedResource>();
      this.autoconfs = new ArrayList<ServerConfiguration.Link>();
      this.sendHeartbeat = true;
      this.serverIsRunning = false;
   }
   
   public XMPPDB getDatabase() {
      return database;
   }
   
   public void setAutoConfigurationWait(long value)
   {
      autoconfWait = value;
   }
   
   public List<ServerConfiguration.Link> getAutoConfiguration()
   {
      return autoconfs;
   }
   
   protected void autoConfigure() {
      final Map<String,Host> newHosts = new TreeMap<String,Host>();
      newHosts.putAll(staticHosts);
      for (ServerConfiguration.Link link : autoconfs) {
         Client client = new Client(org.restlet.data.Protocol.valueOf(link.getURI().getScheme()));
         client.setContext(new Context());
         ChallengeResponse identity = new ChallengeResponse(ChallengeScheme.HTTP_BASIC,link.getUsername(),link.getPassword());
         
         log.info("Loading configuration from "+link.getURI());
         
         try {
            Request request = new Request(Method.GET,new Reference(link.getURI()));
            Response response = client.handle(request);
            if (!response.getStatus().isSuccess()) {
               log.info("Configuration feed not loaded, status="+response.getStatus().getCode());
               if (response.isEntityAvailable()) {
                  log.info(response.getEntityAsText());
               }
               continue;
            }
            DocumentLoader loader = new SAXDocumentLoader();
            Reader feed = response.getEntity().getReader();
            final HandlerEngine engine = new HandlerEngine();
            final List<Document> entries = new ArrayList<Document>();
            engine.push(new Handler() {
               public void init(HandlerEngine engine) {
               }
               public int getStartLevel() {
                  return 0;
               }
               public Handler pop() {
                  return null;
               }
               public void send(Item item) 
                  throws XMLException
               {
                  if (engine.getLevel()==2 && item.getType()==ItemType.ElementItem) {
                     Element e = (Element)item;
                     if (e.getName().equals(ATOM_ENTRY)) {
                        engine.push(new EntryDestination(this,entries),item);
                     }
                  }
               }
            });
            loader.generate(feed, engine);
            feed.close();

            for (Document entry : entries) {
               Iterator<Element> terms = entry.getDocumentElement().getElementsByName(ATOM_CATEGORY);
               List<String> hosts = new ArrayList<String>();
               String auth = null;
               while (terms.hasNext()) {
                  Element term = terms.next();
                  String scheme = term.getAttributeValue("scheme");
                  URI targetTerm = new URI((scheme==null ? "" : scheme)+term.getAttributeValue("term"));
                  if (targetTerm.equals(HOST_T)) {
                     String value = term.getText();
                     value = value.trim();
                     if (value.length()>0) {
                        hosts.add(value);
                     }
                  } else if (targetTerm.equals(AUTH_T)) {
                     auth = term.getText();
                  }
               }
               if (hosts.size()==0) {
                  continue;
               }
               if (auth==null) {
                  Iterator<Element> ids = entry.getDocumentElement().getElementsByName(ATOM_ID);
                  String id = ids.hasNext() ? ids.next().getText() : "unknown";
                  log.warning("Entry "+id+" is missing the auth ("+AUTH_T+") term, skipping...");
                  continue;
               }
               URI authURI = new URI(auth);
               for (String domain : hosts) {
                  Host host = new Host(domain,authURI);
                  log.info("Adding "+domain+" -> "+auth);
                  newHosts.put(domain,host);
               }
            }
            /*
            Response response = client.get(new FeedDestination() {
               public void onFeed(Document feedDoc) {
               }
               public void onEntry(Document entryDoc) {
                  Entry entry = new Entry(entryDoc);
                  entry.index();
                  Term hostT = entry.getTerm(HOST_T);
                  Term authT = entry.getTerm(AUTH_T);
                  if (hostT==null) {
                     log.info("Entry "+entry.getId()+" is missing term "+HOST_T);
                     return;
                  }
                  if (authT==null) {
                     log.info("Entry "+entry.getId()+" is missing term "+AUTH_T);
                     return;
                  }
                  try {
                     URI auth = new URI(authT.getFirstValue());
                     for (String domain : hostT.getValues()) {
                        Host host = new Host(domain,auth);
                        log.info("Adding "+domain+" -> "+auth);
                        newHosts.put(domain,host);
                     }
                  } catch (URISyntaxException ex) {
                     log.severe("Bad auth URI value from autoconfiguration: "+authT.getFirstValue());
                  }
               }
            });
             */
            log.info("Configuration feed status: "+response.getStatus().getCode());
            /*
            Client testClient = new Client(org.restlet.data.Protocol.valueOf(link.getURI().getScheme()));
            testClient.getContext().getAttributes().put("org.restlet.ssl.hostnameVerifier",org.apache.commons.ssl.HostnameVerifier.DEFAULT);
            Response testResponse = testClient.get(link.getURI().toString());
            log.info("Status="+testResponse.getStatus().getCode());
             * 
             */
         } catch (Exception ex) {
            log.log(Level.SEVERE,"Fatal error while getting auto configuration feed at "+link.getURI(),ex);
         }
      }
      hosts = newHosts;
   }
   
   protected ItemDestination getRecipientDestination(ID recipient) {
      ConnectedResource client = connected.get(recipient);
      if (client!=null) {
         return client.getReceiver();
      } else if (recipient.getResource()==null) {
         try {
            User recipientUser = database.getUser(recipient);
            if (recipientUser!=null) {
               Iterator<ID> onlineResources = recipientUser.getResources();
               final List<ItemDestination> sendSet = new ArrayList<ItemDestination>();
               while (onlineResources.hasNext()) {
                  client = connected.get(onlineResources.next());
                  if (client!=null) {
                     sendSet.add(client.getReceiver());
                  }
               }
               if (sendSet.size()>0) {
                  return new ItemDestination() {
                     public void send(Item item)
                        throws XMLException
                     {
                        for (ItemDestination dest : sendSet) {
                           dest.send(item);
                        }
                     }
                  };
               }
            }
         } catch (SQLException ex) {
            log.log(Level.SEVERE,"Database error during recipient lookup.",ex);
         }
      }
      return null;
      
   }
   
   protected void probePresence(User localUser,ID from,ID to)
      throws XMLException
   {
      if (localUser!=null) {
         // we have an online resource
         ConnectedResource client = connected.get(from);
         if (client!=null) {
            client.sendPresence(to, null);
         }
      } else {
         // remote, send to server
         // TODO: implement
      }
   }
   
   protected void notifyPresence(User user,ID resourceId,boolean available)
      throws SQLException,XMLException
   {
      if (!serverIsRunning) {
         return;
      }
      if (user==null) {
         log.warning("user is null");
         return;
      }
      if (resourceId==null) {
         return;
      }
      log.info("Notifying presence for "+resourceId+", "+available);
      ConnectedResource myself = connected.get(resourceId);
      Iterator<ID> online = user.getResources();
      while (online.hasNext()) {
         ID otherResource = online.next();
         if (!otherResource.equals(resourceId)) {
            ConnectedResource client = connected.get(otherResource);
            if (client!=null) {
               client.sendPresence(resourceId,available ? null : "unavailable");
               if (available && myself!=null) {
                  myself.sendPresence(otherResource,null);
               }
            }
         }
      }
      
      if (available) {
         Iterator<ID> toContacts = user.getContactsBySubscription(Contact.Subscription.To);
         while (toContacts.hasNext()) {
            ID notify = toContacts.next();
            log.info("TO: "+notify);
            User notifyUser = database.getUser(notify);
            if (notifyUser!=null) {
               Iterator<ID> notifyResources = notifyUser.getResources();
               while (notifyResources.hasNext()) {
                  ID onlineResource = notifyResources.next();
                  ConnectedResource client = connected.get(onlineResource);
                  if (client!=null) {
                     probePresence(notifyUser,resourceId,onlineResource);
                  }
               }
            } else {
               probePresence(null,resourceId,notify);
            }
         }
      }
      Iterator<ID> fromContacts = user.getContactsBySubscription(Contact.Subscription.From);
      while (fromContacts.hasNext()) {
         ID notify = fromContacts.next();
         log.info("From: "+notify);
         // TODO: optimize with user cache
         User notifyUser = database.getUser(notify);
         if (notifyUser!=null) {
            Iterator<ID> notifyResources = notifyUser.getResources();
            while (notifyResources.hasNext()) {
               ConnectedResource client = connected.get(notifyResources.next());
               if (client!=null) {
                  client.sendPresence(resourceId,available ? null : "unavailable");
               }
            }
         }
      }
      Iterator<ID> bothContacts = user.getContactsBySubscription(Contact.Subscription.Both);
      while (bothContacts.hasNext()) {
         ID notify = bothContacts.next();
         log.info("Both: "+notify);
         // TODO: optimize with user cache
         User notifyUser = database.getUser(notify);
         if (notifyUser!=null) {
            Iterator<ID> notifyResources = notifyUser.getResources();
            while (notifyResources.hasNext()) {
               ID onlineResource = notifyResources.next();
               ConnectedResource client = connected.get(onlineResource);
               if (client!=null) {
                  if (available) {
                     probePresence(notifyUser,resourceId,onlineResource);
                  }
                  client.sendPresence(resourceId,available ? null : "unavailable");
               }
            }
         } else if (available) {
            probePresence(null,resourceId,notify);
         }
      }
   }
   
   protected void connected(ID id,ConnectedResource client)
   {
      connected.put(id,client);
   }
   
   protected void disconnect(ID id,ConnectedResource client)
      throws SQLException
   {
      connected.remove(id);
      client.user.goOffline(id);
   }
   
   protected void streamError(ItemConstructor constructor,ItemDestination stanza,Name condition,String message)
      throws XMLException
   {
      if (constructor==null || stanza==null) {
         return;
      }
      stanza.send(constructor.createElement(Protocol.STREAM_ERROR));
      stanza.send(constructor.createElement(condition));
      stanza.send(constructor.createElementEnd(condition));
      stanza.send(constructor.createElement(Protocol.STREAMS_ERROR_TEXT));
      stanza.send(constructor.createCharacters(message));
      stanza.send(constructor.createElementEnd(Protocol.STREAMS_ERROR_TEXT));
      stanza.send(constructor.createElementEnd(Protocol.STREAM_ERROR));
   }
   
   protected boolean authenticate(Host host,String username,String password)
   {
      Request request = new Request(Method.GET,host.getAuthService().toString()+"?session=false");
      request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_BASIC,username,password));
      Client client = new Client(org.restlet.data.Protocol.valueOf(host.getAuthService().getScheme()));
      //client.getContext().getAttributes().put("org.restlet.ssl.hostnameVerifier",org.apache.commons.ssl.HostnameVerifier.DEFAULT);
      Response response = client.handle(request);
      return response.getStatus().isSuccess();
      
   }
   
   public void setSendHeartbeat(boolean flag)
   {
      sendHeartbeat = flag;
   }
   
   public boolean getSendHeartbeat() 
   {
      return sendHeartbeat;
   }
   
   public void setIdleTimeout(long timeout)
   {
      idleTimeout = timeout;
   }
   
   public long getIdleTimeout()
   {
      return idleTimeout;
   }
   
   public Logger getLogger() {
      return log;
   }
   
   public void setLogger(Logger log)
   {
      this.log = log;
   }
   
   public void setKeystorePath(File file) {
      this.keystorePath = file;
   }
   
   public File getKeystorePath() {
      return keystorePath;
   }
   
   public void setKeystorePassword(String password)
   {
      this.keystorePassword = password;
   }
   
   public String getKeystorePassword() {
      return keystorePassword;
   }
   
   public void setKeyPassword(String password)
   {
      this.keyPassword = password;
   }
   
   public String getKeyPassword() {
      return keyPassword;
   }
   
   public Set<ID> getConnections() {
      return connected.keySet();
   }
   
   public void bind(InetAddress address, int insecure, int secure)
   {
      addresses.add(new BindAddress(address,insecure,secure));
   }
   
   public Map<String,Host> getHosts() {
      return staticHosts;
   }
   
   public void start() 
      throws Exception
   {
      hosts.putAll(staticHosts);
      
      log.info("Start requested.");
      database.offline();
      // Initialize the SSL context
      KeyStore keyStore = KeyStore.getInstance("JKS");
      FileInputStream fis = new FileInputStream(keystorePath);
      keyStore.load(fis, keystorePassword==null ? new char[0] : keystorePassword.toCharArray());
      fis.close();

      KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
      keyManagerFactory.init(keyStore, keyPassword==null ? new char[0] : keyPassword.toCharArray());

      TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
      trustManagerFactory.init(keyStore);

      sslContext = SSLContext.getInstance("TLS");
      sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

      for (BindAddress address : addresses) {
         // insecure pump
         SocketPump pump = new SocketPump(address);
         log.info("Binding to "+(address.address==null ? "*" : address.address)+" as insecure");
         pump.connect();
         pumps.addTask(pump);
         
         // secure pump
         pump = new SSLSocketPump(address);
         log.info("Binding to "+(address.address==null ? "*" : address.address)+" as secure");
         pump.connect();
         pumps.addTask(pump);
      }
      
      serverIsRunning = true;
      
      if (autoconfs.size()>0) {
         autoconfProc = new AutoConfProcess();
         autoconfThread = new Thread(autoconfProc);
         autoconfThread.start();
      }

      log.info("Started.");
   }
   
   public void stop() 
      throws Exception
   {
      log.info("Stop requested.");
      serverIsRunning = false;
      if (autoconfs.size()>0) {
         synchronized (autoconfThread) {
            autoconfThread.interrupt();
         }
      }
      log.info("...stopping pumps...");
      pumps.stop();
      log.info("...stopping client threads...");
      clients.stop();
      database.offline();
      if (autoconfs.size()>0) {
         log.info("...stopping auto configuration...");
         autoconfThread.join(2000);
      }
      
      log.info("Stopped.");
   }
}
